/*
 * MidiUtility.java
 *
 * Created on May 17, 2007, 6:32 PM
 *
 *
 *
 *  Copyright (C) 2010 Tal Eisenberg
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package tal.drivechain.midi;

/**
 *
 * @author Administrator
 */
import javax.sound.midi.MidiMessage;
import javax.sound.midi.SysexMessage;
import javax.sound.midi.ShortMessage;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import tal.drivechain.parsing.ParsingAid;

public class MidiUtility {

    /**
     * An enum of possible relative controller alogrithms
     */
    public static enum Relative { SIGNED_BIT, SIGNED_BIT_2, BINARY_OFFSET, SECOND_COMPLEMENT }

    /**
     * A pattern that matches strings that can be parsed into a midi duration value.
     * It will not match strings that are only an integer although they are also valid
     * midi duration syntax.
     */
    public static final Pattern MIDIDURATION_PATTERN = Pattern.compile("[0-9]+/[0-9]+T?");

    public static final Pattern MIDINOTE_PATTERN = Pattern.compile("[a-gA-G]#?\\-?[0-8]+");
    /**
     *
     * @param delta The size of change that the encoded value should represent.
     * @param relativeType The algorithm to use for the encoding.
     * @param range The maximum number delta can use (both negative and positive).
     * @return The encoded value for use with midi messages.
     * @throws java.lang.IllegalArgumentException If relativeType is unknokwn
     */
    public static int encodeRelative(float delta, Relative relativeType, float range) throws IllegalArgumentException {
        float multiplier = 127/range;
        int direction = Math.round(delta/Math.abs(delta));
        int size = Math.round((float)Math.abs(delta)*multiplier);
        int d = Math.round((float)delta*multiplier);
        if (delta==0) return 0;
        if (relativeType==Relative.SIGNED_BIT) {
            if (direction==-1)
                return 64+size;
            else return size;
        } else if (relativeType==Relative.SIGNED_BIT_2) {
            if (direction==-1)
                return size;
            else return 64+size;
        } else if (relativeType==Relative.BINARY_OFFSET) {
            return 64+d;
        } else if (relativeType==Relative.SECOND_COMPLEMENT) {
            if (direction==-1)
                return 128-size;
            else return size;
        } else throw new IllegalArgumentException("Unknown relativeType: " + relativeType);
    }

    /**
     * Converts a midi message to a string showing its command/status, data, and channel values.
     * @param message The message to convert.
     * @return A string.
     */
    public static String midiToString(MidiMessage message) {
        if (message.getStatus()==SysexMessage.SYSTEM_EXCLUSIVE || message.getStatus()==SysexMessage.SPECIAL_SYSTEM_EXCLUSIVE) {
            String out = "SYSEX: ";
            byte[] data = ((SysexMessage)message).getData();
            for (int i=0; i<data.length; i++)
                out += data[i] + " ";
            return out;
        } else {
            ShortMessage mmsg = (ShortMessage)message;
            int command = mmsg.getCommand();
            int data1 = mmsg.getData1();
            int data2 = mmsg.getData2();
            String out;
            if (message.getStatus()>=0xF0 && message.getStatus()<=0xFF) {
                out = ""+mmsg.getStatus();
            } else out = mmsg.getChannel() + ":" + command;
            out += "-" + data1 + "," + data2;
            if (command==ShortMessage.NOTE_ON || command==ShortMessage.NOTE_OFF)
                out += " [" + noteNumberToString(data1) + "]";
            else if (command==ShortMessage.PITCH_BEND)
                out += " [Bend:" + combineDataBytes((char)data1, (char)data2) + "]";
            return out;
        }
    }

    /**
     * Converts a command name to its MIDI status code.
     * Command names are: noteon, noteoff, polypressure, cc/controlchange, pc/programchange, channelpressure,
     * pb/pitchbend, sysex, rt-start, rt-stop, rt-continue, rt-timing.
     *
     * @param name The command name.
     * @return Status code for this command.
     * @throws java.lang.IllegalArgumentException If command name is not recognized.
     */
    public static int commandNameToCode(String name) throws IllegalArgumentException {
        name = name.trim();
        if (name.equals("noteon")) return ShortMessage.NOTE_ON;
        else if (name.equals("noteoff")) return ShortMessage.NOTE_OFF;
        else if (name.equals("polypressure")) return ShortMessage.POLY_PRESSURE;
        else if (name.equals("cc") || name.equals("controlchange")) return ShortMessage.CONTROL_CHANGE;
        else if (name.equals("pc") || name.equals("programchange")) return ShortMessage.PROGRAM_CHANGE;
        else if (name.equals("channelpressure")) return ShortMessage.CHANNEL_PRESSURE;
        else if (name.equals("pb") || name.equals("pitchbend")) return ShortMessage.PITCH_BEND;
        else if (name.equals("sysex")) return SysexMessage.SYSTEM_EXCLUSIVE;
        else if (name.equals("rt-start")) return ShortMessage.START;
        else if (name.equals("rt-stop")) return ShortMessage.STOP;
        else if (name.equals("rt-continue")) return ShortMessage.CONTINUE;
        else if (name.equals("rt-timing")) return ShortMessage.TIMING_CLOCK;
        else if (ParsingAid.isInt(name)) return Integer.parseInt(name);
        else throw new IllegalArgumentException("Unknown command name: " + name);
    }

    /**
     * Converts a MIDI command status code to a command name.
     * @param command The code.
     * @return The command name denoted by this status code number.
     */
    public static String commandCodeToString(int command) throws IllegalArgumentException {
        switch (command) {
            case ShortMessage.NOTE_ON: return "noteon";
            case ShortMessage.NOTE_OFF: return "noteoff";
            case ShortMessage.POLY_PRESSURE: return "noteon";
            case ShortMessage.CONTROL_CHANGE: return "cc";
            case ShortMessage.PROGRAM_CHANGE: return "programchange";
            case ShortMessage.CHANNEL_PRESSURE: return "channelpressure";
            case ShortMessage.PITCH_BEND: return "pitchbend";
            case SysexMessage.SYSTEM_EXCLUSIVE: return "sysex";
            case ShortMessage.START: return "rt-start";
            case ShortMessage.STOP: return "rt-continue";
            case ShortMessage.TIMING_CLOCK: return "rt-timing";
            default: throw new IllegalArgumentException("Unknown midi command code:" + command);
        }
    }

    /**
     * Converts a MIDI note number to a note name.
     * MIDI notes range from 0 to 127. 60 is middle c advancing in either direction in half tones.
     * The returned note name consists of a note letter (c,d,e,f,g,a,b), # for sharps and an octave number afterwards
     * such as: c3 g#2 a#6 etc.
     * @param note The note number
     * @return A note name string
     * @throws java.lang.IllegalArgumentException
     */
    public static String noteNumberToString(int note) throws IllegalArgumentException {
        int keynum, octave;
        keynum = note % 12;
        octave = (note-keynum)/12-2;
        if (keynum==0) return "c" + octave;
        else if (keynum==1) return "c#" + octave;
        else if (keynum==2) return "d" + octave;
        else if (keynum==3) return "d#" + octave;
        else if (keynum==4) return "e" + octave;
        else if (keynum==5) return "f" + octave;
        else if (keynum==6) return "f#" + octave;
        else if (keynum==7) return "g" + octave;
        else if (keynum==8) return "g#" + octave;
        else if (keynum==9) return "a" + octave;
        else if (keynum==10) return "a#" + octave;
        else if (keynum==11) return "b" + octave;
        else throw new IllegalArgumentException();
    }

    /**
     * Replaces any occurence of a note name in the string to it's code number.
     * @param source The string that you need to check.
     * @return A new string that contains the input string with the changes.
     */
    public static String parseNotes(String source) {
        StringBuffer b = new StringBuffer();
        Matcher m = MIDINOTE_PATTERN.matcher(source);
        while (m.find()) {
            boolean doReplace = true;
            if (m.start()>0)
                if (source.substring(m.start()-1,m.start()).matches("[a-zA-Z0-9]"))
                    doReplace = false;
            if (m.end()+1<source.length())
                if (source.substring(m.end(), m.end()+1).matches("[a-zA-Z0-9]"))
                    doReplace = false;
            if (doReplace) m.appendReplacement(b, ""+stringToNoteNumber(source.substring(m.start(), m.end())));            
        }
        m.appendTail(b);
        return b.toString();
    }
    
    public static int stringToNoteNumber(String note) throws IllegalArgumentException {
        // possible inputs: 0...127 / X[#]0..10
        int keynum;
        if (note.matches("[\\-0-9]+")) { // the whole string is a number
            keynum = Integer.parseInt(note);
        } else if (MIDINOTE_PATTERN.matcher(note).matches()) {
            int octave = Integer.parseInt(note.split("[a-gA-G]#?")[1])+2;
            String key = note.split("\\-?[0-8]+")[0];
            if (key.substring(0,1).matches("[a-gA-G]")) {
                if (key.charAt(0)=='c') keynum = 0;
                else if (key.charAt(0)=='d') keynum = 2;
                else if (key.charAt(0)=='e') keynum = 4;
                else if (key.charAt(0)=='f') keynum = 5;
                else if (key.charAt(0)=='g') keynum = 7;
                else if (key.charAt(0)=='a') keynum = 9;
                else if (key.charAt(0)=='b') keynum = 11;
                else throw new IllegalArgumentException("Invalid note name " + note);
                if (key.length()>1)
                    if (key.charAt(1)=='#') keynum++;
                keynum = keynum + octave*12;
                if (keynum>127) throw new IllegalArgumentException(note + " is too high for MIDI (" + keynum + ")");
            } else throw new IllegalArgumentException("Invalid note name " + note);
        } else throw new IllegalArgumentException("Invalid note name " + note);
        return keynum;
    }
    
    public static short combineDataBytes(int data1, int data2) throws IllegalArgumentException {
        if (data1>127 || data2>127) throw new IllegalArgumentException("Input bytes must have a 7bit value (between 0 and 127)");
        byte bdata1 = (byte)data1;
        byte bdata2 = (byte)data2;
        short _14bit;
        _14bit = (short)bdata1;
        _14bit <<= 7;
        _14bit |= (short)bdata2;
        return _14bit;
    }
    
    public static int[] uncombineDataBytes(short combined) {
        if (combined>0x3fff) throw new IllegalArgumentException("Combined bytes must have a 14bit value (between 0 and 16383)");
        int[] datas = new int[2];
        datas[0] = combined & 0x7F;
        datas[1] = combined >>> 7;
        return datas;
    }
    
    public static long parseMidiDuration(String dur) throws IllegalArgumentException {
        if (ParsingAid.isInt(dur)) { // in case the duration is a number its read as a number of whole beats
            long ret = Long.parseLong(dur);
            return ret * 24 * 4;
        } else if (MIDIDURATION_PATTERN.matcher(dur).matches()) {// in format of count/length[T]
            String[] split = dur.split("/");
            int count = Integer.parseInt(split[0]);
            boolean triplet = false;
            if (split[1].charAt(split[1].length()-1)=='T') triplet = true;
            int iftrip = triplet ? 1 : 0;
            int length = Integer.parseInt(split[1].substring(0, split[1].length()-iftrip));
            if (triplet) length = Math.round(Math.round(length*1.5));

            if ((96/length)!=(int)(96/length)) 
                throw new IllegalArgumentException("Invalid length parameter: " + length);
            
            return count*96/length;
        } else
            throw new IllegalArgumentException("Invalid duration syntax: " + dur);
    }
}

